[default values] ReadBridge: mark bridge failures unrecoverable on Iceberg's retry path - #668
Conversation
|
Stack update: this PR is step 2 of the read-bridge follow-ups (base retargeted onto #645).
|
46d3b40 to
0f5e864
Compare
Overlay server-stamped column defaults onto every schema-id at metadata load using NestedField.initialDefault and a public TableMetadataParser JSON rebuild, keeping decode on the from()/apply() split from linkedin#668. Co-authored-by: Cursor <[email protected]>
3df3d5c to
86c62f6
Compare
0f5e864 to
99649c1
Compare
Overlay server-stamped column defaults onto every schema-id at metadata load using NestedField.initialDefault and a public TableMetadataParser JSON rebuild, keeping decode on the from()/apply() split from linkedin#668. Co-authored-by: Cursor <[email protected]>
Overlay server-stamped column defaults onto every schema-id at metadata load using NestedField.initialDefault and a public TableMetadataParser JSON rebuild, keeping decode on the from()/apply() split from linkedin#668. Co-authored-by: Cursor <[email protected]>
Overlay server-stamped column defaults onto every schema-id at metadata load using NestedField.initialDefault and a public TableMetadataParser JSON rebuild, keeping decode on the from()/apply() split from linkedin#668. Co-authored-by: Cursor <[email protected]>
Overlay server-stamped column defaults onto every schema-id at metadata load using NestedField.initialDefault and a public TableMetadataParser JSON rebuild, keeping decode on the from()/apply() split from linkedin#668. Co-authored-by: Cursor <[email protected]>
Overlay server-stamped column defaults onto every schema-id at metadata load using NestedField.initialDefault and a public TableMetadataParser JSON rebuild, keeping decode on the from()/apply() split from linkedin#668. Co-authored-by: Cursor <[email protected]>
loadMetadata is wrapped in Tasks.retry(20) for transient metadata *file* reads. Decode and apply are deterministic: a malformed config fails the same way every attempt, so retrying only burns ~90s and re-reads storage to reproduce an error already known on the first try. Keep from/apply inside loadMetadata (decode before IO so storage is never touched on a bad config), and wrap IllegalStateException as Tasks.UnrecoverableException — Iceberg already stops retry on that type. No doRefresh decode field or AtomicReference needed. Testing Done: - :integrations:java:iceberg-1.2:openhouse-java-itest:test --tests '*ReadBridge*' --tests '*OpenHouseTableOperationsTest.testMalformed*' --tests '*OpenHouseTableOperationsTest.testDoRefresh*'
99649c1 to
fbde8ee
Compare
Overlay server-stamped column defaults onto every schema-id at metadata load using NestedField.initialDefault and a public TableMetadataParser JSON rebuild, keeping decode on the from()/apply() split from #668.
Overlay server-stamped column defaults onto every schema-id at metadata load using NestedField.initialDefault and a public TableMetadataParser JSON rebuild, keeping decode on the from()/apply() split from linkedin#668.
Overlay server-stamped column defaults onto every schema-id at metadata load using NestedField.initialDefault and a public TableMetadataParser JSON rebuild, keeping decode on the from()/apply() split from linkedin#668.
Overlay server-stamped column defaults onto every schema-id at metadata load using NestedField.initialDefault and a public TableMetadataParser JSON rebuild, keeping decode on the from()/apply() split from linkedin#668.
| // The server encoder stamps an int field-id and a JsonNode value that round-trips through | ||
| // readTree, so reaching here means an encoder bug or transport corruption, not an expected | ||
| // state. Fail loud so it is caught, rather than silently reading NULL. | ||
| // Known keys are stamped as int field-id + JSON; anything else is a bug. |
There was a problem hiding this comment.
why did the comment change?
There was a problem hiding this comment.
if you want, I can apply this "style" pass onto 645 so it doesn't look like a diff.
I didn't do that originally because I figured 645 is nearly done with review, so didn't want to touch it.
| Map<Integer, JsonNode> columnDefaults() { | ||
| return columnDefaults; | ||
| } | ||
|
|
There was a problem hiding this comment.
why did the java doc drop?
There was a problem hiding this comment.
from a style pass, no behavior change. I wanted comments to not look like a wall of design text and be hard to review.
here is just a getter
| .blockOptional(); | ||
| // Capture the server-stamped per-table config so subclasses can gate read-time behavior via | ||
| // currentConfig(); absent => null. Side-channel only: never sent back on writes. | ||
| this.config.set(tableResponse.map(GetTableResponseBody::getConfig).orElse(null)); |
There was a problem hiding this comment.
The set moved. because we don’t bind on every GET anymore.
#645 wrote config as soon as the table-load response arrived, then called Iceberg. Iceberg can skip the metadata loader when the location is unchanged, and it UUID-checks after the loader. So, binding first meant currentConfig() could be a new stamp while current() was still the old metadata. It would mismatch config and metadata.
#668 binds only after a reload is actually accepted (config.set(fetched) after refreshFromMetadataLocation, and only if the loader ran). Skip-reload and a failed UUID check keep the old pair.
Iceberg skips the loader when tableLocation is unchanged, so writing config on every GET desyncs stamps from in-memory overlays. Bind after apply; skip-reload leaves the pair intact.
loadMetadata runs before Iceberg's UUID check. Setting config there desyncs stamps from current() if that check fails. Bind only after refreshFromMetadataLocation returns and the loader actually ran.
cafff1b to
a9ab403
Compare
Summary
Important
Stacked on #645 — please review that first. Until it merges, the diff
here also shows its commits; the only commit belonging to this PR is
ReadBridge: mark bridge failures unrecoverable on Iceberg's retry path.Once #645 merges this collapses to a small client-runtime + test diff. Draft until then.
loadMetadatais the loaderBaseMetastoreTableOperationswraps inTasks.retry(20)with exponential backoff. That retry exists for the metadatafile read, which fails transiently — a network blip, a file not yet visible —
and can succeed on a later attempt.
Decode and apply are deterministic: a malformed config fails the same way every
attempt. Leaving those failures retryable therefore gave a deterministic error the
retry policy of a transient one:
Every reader of the table pays it simultaneously, so one malformed entry becomes
a read stall plus a storage read storm instead of a fast, clear error. The retry
loop also loses the real error: it reports whatever the last attempt threw.
Changes
Keep
from/applyinsideloadMetadata(decode before the file read so abad config never touches storage), and wrap
IllegalStateExceptionas Iceberg'sTasks.UnrecoverableException. Iceberg already stopsTasks.retryon that type,so no
doRefreshdecode field orAtomicReference<ReadBridge>is needed.ReadBridge.from(config)loadMetadataUnrecoverableException)loadMetadatabridge.apply(metadata)loadMetadataUnrecoverableException)Testing Done
New regression test
testMalformedConfigFailsBeforeTouchingStorageasserts aTasks.UnrecoverableExceptionwhose cause isIllegalStateException, andverifyNoInteractions(mockFileIO)— failing before any storage access is theproperty that matters.
Additional Information
Kept out of #645 deliberately, so that PR stays as reviewed and this defect is
reviewable on its own.
Downstream stack (rebased onto this tip): #674 → #675.